<!--
@license
Copyright 2017 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-backend/tf-backend.html">
<link rel="import" href="../tf-color-scale/tf-color-scale.html">
<link rel="import" href="../vz-line-chart/vz-line-chart.html">

<!--
  A component that fetches data from the TensorBoard server and renders it into
  a vz-line-chart.
-->
<dom-module id="tf-line-chart-data-loader">
  <template>
    <div id="chart-and-spinner-container">
      <vz-line-chart
        x-components-creation-method="[[xComponentsCreationMethod]]"
        x-type="[[xType]]"
        y-value-accessor="[[yValueAccessor]]"
        color-scale="[[colorScale]]"
        tooltip-columns="[[tooltipColumns]]"
        smoothing-enabled="[[smoothingEnabled]]"
        smoothing-weight="[[smoothingWeight]]"
        tooltip-sorting-method="[[tooltipSortingMethod]]"
        ignore-y-outliers="[[ignoreYOutliers]]"
        style="[[_computeLineChartStyle(_loading)]]"
        default-x-range="[[defaultXRange]]"
        default-y-range="[[defaultYRange]]"
        fill-area="[[fillArea]]"
        symbol-function="[[symbolFunction]]"
      ></vz-line-chart>
      <template is="dom-if" if="[[_loading]]">
        <div id="loading-spinner-container">
          <paper-spinner-lite active></paper-spinner-lite>
        </div>
      </template>
    </div>
    <style>
      :host {
        height: 100%;
        width: 100%;
        display: flex;
        flex-direction: column;
      }

      :host[_maybe-rendered-in-bad-state] vz-line-chart {
        visibility: hidden;
      };

      #chart-and-spinner-container {
        display: flex;
        flex-grow: 1;
        position: relative;
      }

      #loading-spinner-container {
        align-items: center;
        bottom: 0;
        display: flex;
        display: flex;
        justify-content: center;
        left: 0;
        pointer-events: none;
        position: absolute;
        right: 0;
        top: 0;
      }

      vz-line-chart {
        -webkit-user-select: none;
        -moz-user-select: none;
      }
    </style>
  </template>
  <script>
    (function() {

    // The chart can sometimes get in a bad state, when it redraws while
    // it is display: none due to the user having switched to a different
    // page. This code implements a cascading queue to redraw the bad charts
    // one-by-one once they are active again.
    // We use a cascading queue becuase we don't want to block the UI / make the
    // ripples very slow while everything synchronously redraws.
    const redrawQueue = [];
    const cascadingRedraw = _.throttle(function internalRedraw() {
      if (redrawQueue.length > 0) {
        const x = redrawQueue.shift();
        if (x.active) {
          x.redraw();
          x._maybeRenderedInBadState = false;
        }
        window.setTimeout(internalRedraw, 32);
      }
    }, 100);

    Polymer({
      is: 'tf-line-chart-data-loader',
      properties: {
        // An array of selected runs (strings).
        runs: Array,
        tag: String,

        // An optional list of data series. Each data series is a string that
        // corresponds to a line in the chart. If this property is not provided,
        // the list of data series consists of the list of runs.
        dataSeries: {
          type: Array,
          value: [],
        },

        xComponentsCreationMethod: Object,
        xType: String,
        yValueAccessor: Object,
        tooltipColumns: Array,
        fillArea: Object,

        smoothingEnabled: Boolean,
        smoothingWeight: Number,
        tooltipSortingMethod: String,
        ignoreYOutliers: Boolean,

        defaultXRange: Array,
        defaultYRange: Array,

        symbolFunction: Object,

        /**
         * A function that takes as inputs:
         * 1. This tf-line-chart-data-loader component.
         * 2. The run (string) the response is relevant to.
         * 3. The response received from the data URL.
         * This function will be called when a response from a request to that
         * data URL is successfully received.
         * @type {Function}
         */
        processData: Object,

        active: {
          type: Boolean,
          observer: '_fixBadStateWhenActive',
        },

        requestManager: Object,

        // A function that takes 2 string arguments (tag and run) and returns a
        // string URL for fetching data.
        dataUrl: Function,

        logScaleActive: {
          type: Boolean,
          observer: '_logScaleChanged',
        },

        /**
         * An object with a scale method that maps from data series to color.
         */
        colorScale: {
          type: Object,
          value: () => ({scale: tf_color_scale.runsColorScale}),
        },

        _loading: {
          type: Boolean,
          value: false,
        },

        _resetDomainOnNextLoad: {
          type: Boolean,
          value: true,
        },

        _canceller: {
          type: Object,
          value: () => new tf_backend.Canceller(),
        },

        /*
         * A map such that `_loadedRuns[run] === true` iff we've loaded
         * `run` already, or are in the process of loading it. This
         * exists so that when there are 100 runs selected and the user
         * selects an additional run, we only fetch 1 more run instead
         * of 101.
         *
         * `reload` clears this cache.
         *
         * Equivalently, this is the set of runs for which, after
         * callbacks in-flight resolve, the most recent call to
         * `setSeriesData(run, data)` on the chart object (a) exists
         * and (b) occurred after all reloads so far.
         */
        _loadedRuns: {
          type: Object,
          value: () => ({}),
        },

        _maybeRenderedInBadState: {
          type: Boolean,
          value: false,
          reflectToAttribute: true,
        },
      },
      observers: [
        '_tagChanged(_attached, tag)',
        '_dataChanged(_attached, dataSeries, runs.*)'
      ],
      created() {
        this._loadData = _.debounce(
          this._loadData, 100, {leading: true, trailing: true});
      },
      attached() {
        this._attached = true;
        this._changeSeries();
      },
      reload() {
        this._loadedRuns = {};
        this._loadData();
      },
      resetDomain() {
        const chart = this.$$('vz-line-chart');
        if (chart) {
          chart.resetDomain();
        }
      },
      setSeriesData(run, data) {
        this.$$('vz-line-chart').setSeriesData(run, data);
      },
      redraw() {
        this.$$('vz-line-chart').redraw();
      },
      _tagChanged(attached, tagUpdateRecord) {
        this._loadedRuns = {};
        this._resetDomainOnNextLoad = true;
        this._loadData();
      },
      _dataChanged(attached, dataSeries, runsUpdateRecord) {
        if (!attached) {
          return;
        }

        let visibleSeries = dataSeries;
        if (visibleSeries.length === 0) {
          // The data series is not explicitly set. Default to using runs.
          visibleSeries = runsUpdateRecord.base;
        }
        this.$$('vz-line-chart').setVisibleSeries(visibleSeries);
        this._loadData();
      },
      _loadData() {
        this.async(() => {
          if (!this._attached) {
            return;
          }
          this._loading = true;
          //
          // Before updating, cancel any network-pending updates, to
          // prevent race conditions where older data stomps newer data.
          this._canceller.cancelAll();
          const tag = this.tag;
          const runPromises = this.runs.map(run => {
            if (this._loadedRuns[run]) {
              return Promise.resolve();
            }
            const url = this.dataUrl(tag, run);
            const updateSeries = this._canceller.cancellable(result => {
              if (result.cancelled) {
                return;
              }
              if (tag === this.tag) {
                // Only update the runs cache for the current tag. If we
                // load data for Tag A, then the tag changes to Tag B
                // while requests are still in flight, these requests
                // should not poison the cache.
                this._loadedRuns[run] = true;
              }
              this.processData(this, run, result.value);
            });
            return this.requestManager.request(url).then(updateSeries);
          });
          const finish = this._canceller.cancellable(result => {
            if (!result.cancelled) {
              this._loading = false;
              const chart = this.$$('vz-line-chart');
              if (runPromises.length > 0 && this._resetDomainOnNextLoad) {
                // (Don't unset _resetDomainOnNextLoad when we didn't
                // load any runs: this has the effect that if all our
                // runs are deselected, then we toggle them all on, we
                // properly size the domain once all the data is loaded
                // instead of just when we're first rendered.)
                this._resetDomainOnNextLoad = false;
                chart.resetDomain();
              }
              if (!this.active) {
                // If we reached a point where we should render while the page
                // is not active, we've gotten into a bad state.
                this._maybeRenderedInBadState = true;
              } else {
                this.redraw();
              }
            }
          });
          return Promise.all(runPromises).then(finish);
        });
      },
      _changeSeries() {
        let visibleSeries = this.dataSeries;
        if (this.dataSeries.length === 0) {
          // The data series is not explicitly set. Default to using runs.
          visibleSeries = this.runs;
        }

        this.$$('vz-line-chart').setVisibleSeries(visibleSeries);
        this._loadData();
      },
      _logScaleChanged(logScaleActive) {
        var chart = this.$$('vz-line-chart');
        chart.yScaleType = logScaleActive ? 'log' : 'linear';
        this.redraw();
      },
      _computeLineChartStyle(loading) {
        return loading ? 'opacity: 0.3;' : '';
      },

      _fixBadStateWhenActive() {
        // When the chart enters a potentially bad state (because it should
        // redraw, but the page is not currently active), we set the
        // _maybeRenderedInBadState flag. Whenever the chart becomes active,
        // we test this and schedule a redraw of the bad charts.
        if (this.active && this._maybeRenderedInBadState) {
          redrawQueue.push(this);
          cascadingRedraw();
        }
      },
    });
    })();
  </script>
</dom-module>
